/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package commons.util;

import org.apfloat.Apfloat;
import org.apfloat.ApintMath;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Some static utility methods are missing from ApfloatMath, such as Min and Max, present in BigDecimal.
 * This class collects such methods.
 *
 * @since 1.1.1
 * @version 1.1.1
 * @author Susanta Tewari
 * @history Created on Aug 6, 2012.
 */
public class ApfloatUtil {

    /** cache for descending factorial computation */
    private static final Map<String, BigDecimal> desc_factorial_cache = new ConcurrentHashMap<>(50);

    /** cache for ascending factorial computation */
    private static final Map<String, BigDecimal> asce_factorial_cache = new ConcurrentHashMap<>(50);

    /** cache for factorial computation */
    private static final Map<Integer, BigInteger> factorial_cache = new ConcurrentHashMap<>(75);

    /** default math rounding mode */
    public static final MathContext MATHCONTEXT_128_HALF_UP = new MathContext(128,
                                                                  RoundingMode.HALF_UP);

    /**
     * Prevents instantiation.
     */
    private ApfloatUtil() {}

    /**
     * Returns min(a,b) i.e., {@code a.compareTo(b)} &le; {@code 0 ? a : b}
     *
     * @param a value a
     * @param b value b
     * @return minimum of a and b
     */
    public static Apfloat Min(Apfloat a, Apfloat b) {
        return ((a.compareTo(b) <= 0) ? a : b);
    }

    /**
     * Returns max(a,b) i.e., {@code a.compareTo(b)} &ge; {@code 0 ? a : b}
     *
     * @param a value a
     * @param b value b
     * @return maximum of a and b
     */
    public static Apfloat Max(Apfloat a, Apfloat b) {
        return ((a.compareTo(b) >= 0) ? a : b);
    }

    /**
     * Converts {@code Apfloat} to the corresponding {@code BigDecimal} value.
     *
     * @param x value value in {@code Apfloat}
     * @return the corresponding {@code BigDecimal}
     */
    public static BigDecimal toBigDecimal(Apfloat x) {
        return new BigDecimal(x.toString());
    }

    /**
     * Caches factorial values.
     *
     * @param n value
     * @return factorial (arbitrary precision) for {@code n}
     */
    public static BigInteger factorial(final int n) {

        if (!factorial_cache.containsKey(n)) {

            final BigInteger factorial = ApintMath.factorial(n).toBigInteger();

            factorial_cache.put(n, factorial);
        }

        return factorial_cache.get(n);
    }

    /**
     * Implements n_sup (m_bar) = n(n-1)...(n-m+1)
     *
     * @param n parameter {@code n} in the formula above
     * @param m parameter {@code m} in the formula above
     * @return result of the formula above
     */
    public static BigDecimal descendingFactorial(final double n, final int m) {

        final String key = n + "-" + m;

        if (!desc_factorial_cache.containsKey(key)) {

            BigDecimal result = BigDecimal.valueOf(1.0);

            for (int i = 0; i < m; i++) {
                result = result.multiply(new BigDecimal(n - i));
            }

            desc_factorial_cache.put(key, result);
        }

        return desc_factorial_cache.get(key);
    }

    /**
     * Implements n_sup (m_bar) = n(n+1)...(n+m-1)
     *
     * @param n parameter {@code n} in the formula above
     * @param m parameter {@code m} in the formula above
     * @return result of the formula above
     */
    public static BigDecimal ascendingFactorial(final double n, final int m) {

        final String key = n + "-" + m;

        if (!asce_factorial_cache.containsKey(key)) {

            BigDecimal result = BigDecimal.valueOf(1.0);

            for (int i = 0; i < m; i++) {
                result = result.multiply(new BigDecimal(n + i));
            }

            asce_factorial_cache.put(key, result);
        }

        return asce_factorial_cache.get(key);
    }

    public static BigDecimal choose(final int n, final int m) {

        return descendingFactorial(n, m).divide(new BigDecimal(factorial(m)),
                                   MATHCONTEXT_128_HALF_UP);
    }
}
